/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.devtools.j2objc.ast;

import com.google.devtools.j2objc.GenerationTest;
import java.io.IOException;

/**
 * Unit tests for {@link LambdaExpression}.
 *
 * @author Seth Kirby
 */
public class LambdaExpressionTest extends GenerationTest {

  private String functionHeader = "interface Function<T, R> { R apply(T t); }";
  private String fourToOneHeader =
      """
      interface FourToOne<F, G, H, I, R> {
        R apply(F f, G g, H h, I i);
      }
      """;

  public void testCaptureDetection() throws IOException {
    translateSourceFile(
        functionHeader
            + """
            interface A {
              boolean r(boolean a);
              default int ret1() {
                return 2;
              }
            }
            interface B {
              default int ret() {
                return 1;
              }
            }
            class Test {
              void f() {
                ((A & B) (a) -> a).ret();
              }
            }
            """,
        "Test",
        "Test.m");
    translateSourceFile(
        functionHeader
            + """
            interface A2 {
              boolean r();
              default int ret1() {
                return 2;
              }
            }
            interface B {
              default int ret() {
                return 1;
              }
            }
            class Test {
              void f() {
                ((A2 & B) () -> true).ret();
              }
            }
            """,
        "Test",
        "Test.m");
    String nonCaptureTranslation =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  Function f = x -> x;
                }
                """,
            "Test",
            "Test.m");
    String captureTranslationOuter =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  int y;
                  Function f = x -> y;
                }
                """,
            "Test",
            "Test.m");
    String captureTranslation =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  Function<Function, Function> f = y -> x -> y;
                }
                """,
            "Test",
            "Test.m");
    assertInTranslation(nonCaptureTranslation, "Test_$Lambda$1_get_instance(void);");
    assertTranslatedLines(captureTranslationOuter,
        "@interface Test_$Lambda$1 : NSObject < Function > {",
        " @public",
        "  Test *this$0_;",
        "}");
    assertInTranslation(captureTranslation, "Test_$Lambda$1_get_instance(void);");
    assertTranslatedLines(captureTranslation,
        "@interface Test_$Lambda$2 : NSObject < Function > {",
        " @public",
        "  id<Function> val$y_;",
        "}");
  }

  public void testTypeInference() throws IOException {
    String quadObjectTranslation =
        translateSourceFile(
            fourToOneHeader
                + """
                class Test {
                  FourToOne f = (a, b, c, d) -> 1;
                }
                """,
            "Test",
            "Test.m");
    assertTranslatedLines(quadObjectTranslation,
        "- (id)applyWithId:(id)a",
        "           withId:(id)b",
        "           withId:(id)c",
        "           withId:(id)d {",
        "  return JavaLangInteger_valueOfWithInt_(1);",
        "}");
    String mixedObjectTranslation =
        translateSourceFile(
            fourToOneHeader
                + """
                class Test {
                  FourToOne<String, Double, Integer, Boolean, String> f = (a, b, c, d) -> "1";
                }
                """,
            "Test",
            "Test.m");
    assertTranslatedLines(mixedObjectTranslation,
        "- (id)applyWithId:(NSString *)a",
        "           withId:(JavaLangDouble *)b",
        "           withId:(JavaLangInteger *)c",
        "           withId:(JavaLangBoolean *)d {",
        "  return @\"1\";",
        "}");
  }

  public void testOuterFunctions() throws IOException {
    String translation =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  Function outerF = (x) -> x;
                }
                """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "JreStrongAssign(&self->outerF_, JreLoadStatic(Test_$Lambda$1, instance));");
  }

  public void testStaticFunctions() throws IOException {
    String translation =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  static Function staticF = (x) -> x;
                }
                """,
            "Test",
            "Test.m");
    assertTranslatedSegments(translation,
        "id<Function> Test_staticF;",
        "if (self == [Test class]) {",
        "JreStrongAssign(&Test_staticF, JreLoadStatic(Test_$Lambda$1, instance))");
  }

  public void testNestedLambdas() throws IOException {
    String outerCapture =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  Function<String, Function<String, String>> f = x -> y -> x;
                }
                """,
            "Test",
            "Test.m");
    assertInTranslation(outerCapture, "Test_$Lambda$1_get_instance(void);");
    assertTranslatedLines(outerCapture,
        "- (id)applyWithId:(NSString *)x {",
        "  return create_Test_$Lambda$2_initWithNSString_(x);",
        "}");
    assertTranslatedLines(outerCapture,
        "@interface Test_$Lambda$2 : NSObject < Function > {",
        " @public",
        "  NSString *val$x_;",
        "}");
    assertTranslatedLines(outerCapture,
        "- (id)applyWithId:(NSString *)y {",
        "  return JreRetainedLocalValue(val$x_);",
        "}");
    String noCapture =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  Function<String, Function<String, String>> f = x -> y -> y;
                }
                """,
            "Test",
            "Test.m");
    assertInTranslation(noCapture, "Test_$Lambda$1_get_instance(void);");
    assertInTranslation(noCapture, "Test_$Lambda$2_get_instance(void);");
    assertTranslatedLines(noCapture,
        "- (id)applyWithId:(NSString *)x {",
        "  return JreRetainedLocalValue(JreLoadStatic(Test_$Lambda$2, instance));",
        "}");
    assertTranslatedLines(noCapture,
        "- (id)applyWithId:(NSString *)y {",
        "  return JreRetainedLocalValue(y);",
        "}");
  }

  // There's no need for a cast_check call on a lambda whose type matches the assigned type.
  public void testNoCastCheck() throws IOException {
    String translation =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  Function f = (Function) (x) -> x;
                }
                """,
            "Test",
            "Test.m");
    assertNotInTranslation(translation, "cast_check");
  }

  // Test that we aren't trying to import lambda types.
  public void testImportExclusion() throws IOException {
    String translation =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  Function f = (Function) (x) -> x;
                }
                """,
            "Test",
            "Test.m");
    assertNotInTranslation(translation, "lambda$0.h");
  }

  // Check that lambdas are uniquely named.
  public void testLambdaUniquify() throws IOException {
    String translation =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  class Foo {
                    class Bar {
                      Function f = x -> x;
                    }
                  }
                  Function f = x -> x;
                }
                """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "@interface Test_Foo_Bar_$Lambda$1 : NSObject < Function >");
    assertInTranslation(translation, "@interface Test_$Lambda$1 : NSObject < Function >");
  }

  // Check that lambda captures respect reserved words.
  public void testLambdaCloseOverReservedWord() throws IOException {
    String translation =
        translateSourceFile(
            functionHeader
                + """
                class Test {
                  void f(int operator) {
                    Function l = (a) -> operator;
                  }
                }
                """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "val$operator_");
  }

  public void testLargeArgumentCount() throws IOException {
    String interfaceHeader =
        """
        interface TooManyArgs<T> {
          T f(
              T a, T b, T c, T d, T e, T f, T g, T h, T i, T j, T k, T l, T m, T n, T o, T p, T q,
              T r, T s, T t, T u, T v, T w, T x, T y, T z, T aa, T ab, T ac, T ad, T ae, T af, T ag,
              T ah, T ai, T aj, T ak, T al, T am, T an, T ao, T ap, T aq, T ar, T as, T at, T au,
              T av, T aw, T ax, T ay, T az, T ba, T bb, T bc, T bd, T be, T bf, T bg, T bh, T bi,
              T bj, T bk, T bl, T bm, T bn, T bo, T bp, T bq, T br, T bs, T bt, T bu, T bv, T bw,
              T bx, T by, T bz, T foo);
        }
        """;
    String translation =
        translateSourceFile(
            interfaceHeader
                + """
                class Test {
                  void a() {
                    Object foo = "Foo";
                    TooManyArgs fun =
                        (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y,
                            z, aa, ab, ac, ad, ae, af, ag, ah, ai, aj, ak, al, am, an, ao, ap, aq,
                            ar, as, at, au, av, aw, ax, ay, az, ba, bb, bc, bd, be, bf, bg, bh, bi,
                            bj, bk, bl, bm, bn, bo, bp, bq, br, bs, bt, bu, bv, bw, bx, by, bz,
                            bar) ->
                            foo;
                  }
                }
                """,
            "Test",
            "Test.m");
    assertTranslatedLines(translation,
        "- (id)fWithId:(id)a",
        "       withId:(id)b",
        "       withId:(id)c",
        "       withId:(id)d",
        "       withId:(id)e");
    assertTranslatedLines(translation,
        "       withId:(id)bw",
        "       withId:(id)bx",
        "       withId:(id)by",
        "       withId:(id)bz",
        "       withId:(id)bar {",
        "  return JreRetainedLocalValue(val$foo_);",
        "}");
  }

  public void testCapturingBasicTypeReturn() throws IOException {
    String header = "interface I { int foo(); }";
    String translation =
        translateSourceFile(
            header
                + """
                class Test {
                  int f = 1234;
                  void foo() {
                    I i = () -> f;
                  }
                }
                """,
            "Test",
            "Test.m");
    assertTranslatedLines(translation,
        "- (int32_t)foo {",
        "  return this$0_->f_;",
        "}");
  }

  // Verify that an #include is generated for the lambda's functionalType.
  public void testLambdaFunctionalTypeImport() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.*;
            class Test {
              public void test(List<String> names) {
                Collections.sort(names, (p1, p2) -> p1.compareTo(p2));
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "#include \"java/util/Comparator.h\"");
  }

  public void testClassLiteralNamesInLambda() throws IOException {
    String translation =
        translateSourceFile(
            """
            package foo.bar;
            import java.io.Serializable;
            interface Comparator<T> {
              int compare(T o1, T o2);
              default Comparator<T> thenComparing(Comparator<? super T> other) {
                return (Comparator<T> & Serializable) (c1, c2) -> {
                  int res = compare(c1, c2);
                  return (res != 0) ? res : other.compare(c1, c2);
                };
              }
            }
            """,
            "Comparator",
            "foo/bar/Comparator.m");
    assertTranslatedLines(translation,
        "@interface FooBarComparator_$Lambda$1 : "
            + "NSObject < FooBarComparator, JavaIoSerializable > {",
        " @public",
        "  id<FooBarComparator> this$0_;",
        "  id<FooBarComparator> val$other_;",
        "}");
    assertTranslatedLines(translation,
        "- (int32_t)compareWithId:(id)c1",
        "               withId:(id)c2 {",
        "  int32_t res = [this$0_ compareWithId:c1 withId:c2];",
        "  return (res != 0) ? res : [((id<FooBarComparator>) nil_chk(val$other_)) "
            + "compareWithId:c1 withId:c2];",
        "}");
  }
}
